/**
 * Copyright (C) 2008 GeoScheduler Team, as stated on <http://www.geoscheduler.org/authors>.
 * 
 * This file is part of GeoScheduler.
 * 
 * GeoScheduler is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * GeoScheduler is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with GeoScheduler. If not, see <http://www.gnu.org/licenses/>.
 */

package org.geoscheduler.commons.marshaller.impl;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.geoscheduler.commons.marshaller.JSONField;
import org.geoscheduler.commons.marshaller.JSONFieldDecl;
import org.geoscheduler.commons.marshaller.JSONFieldDeclFactory;
import org.geoscheduler.commons.marshaller.JSONMarshaller;
import org.geoscheduler.commons.tools.PropertyDescriptor;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * 
 * <h2>JSONMarshallerImpl</h2>
 * <p>
 * Controller class for {@link JSONFieldDecl} instances. Class implements API
 * for marsahlling/unmarshalling object trees to JSON notation using annotation
 * metadata.
 * </p>
 * <p>
 * Implementation does not use classes from java.beans packages of JDK to be
 * portable to Android platform.
 * </p>
 * <p>
 * Introspection on annotated classes is done only once and stored in cache.
 * Second time when such type is marshalled or unmarshalled introspection data
 * is get from cache.
 * </p>
 * 
 * <p>
 * TODO: cache structure of cache to reflect type hierarchy
 * </p>
 * 
 * @author lko
 */
public class JSONMarshallerImpl implements JSONMarshaller {

	/** Inner helper structure to hold data in cache */
	protected static class TypeMarshallingDescriptor implements Iterable<JSONFieldDecl<?>> {

		private final Class<?> type;
		private final List<JSONFieldDecl<?>> fieldDecls = new ArrayList<JSONFieldDecl<?>>();

		public TypeMarshallingDescriptor(Class<?> type) {
			this.type = type;
		}

		public List<JSONFieldDecl<?>> getFieldDecls() {
			return fieldDecls;
		}

		public void addFieldDecl(JSONFieldDecl<?> handler) {
			fieldDecls.add(handler);
		}

		public Iterator<JSONFieldDecl<?>> iterator() {
			return fieldDecls.listIterator();
		}

		public Class<?> getType() {
			return type;
		}
	}

	private final Map<Class<?>, TypeMarshallingDescriptor> cache = new HashMap<Class<?>, TypeMarshallingDescriptor>();
	private final List<JSONFieldDeclFactory<?>> factories;

	public JSONMarshallerImpl(List<JSONFieldDeclFactory<?>> factories) {
		this.factories = factories;
	}

	/**
	 * @see org.geoscheduler.commons.marshaller.JSONMarshaller#marshall(java.lang.Object,
	 *      org.json.JSONObject)
	 */
	public JSONObject marshall(Object instance, JSONObject obj) throws JSONException {
		if (instance == null) {
			return null;
		}
		TypeMarshallingDescriptor desc = getDescriptor(instance);
		for (JSONFieldDecl<?> decl : desc) {
			decl.marshall(this, instance, obj);
		}
		return obj;
	};

	/**
	 * @see org.geoscheduler.commons.marshaller.JSONMarshaller#unmarshall(java.lang.Object,
	 *      org.json.JSONObject)
	 */
	public <T> T unmarshall(T instance, JSONObject obj) throws JSONException {
		TypeMarshallingDescriptor desc = getDescriptor(instance);
		for (JSONFieldDecl<?> handler : desc) {
			handler.unmarshall(this, instance, obj);
		}
		return instance;
	};

	protected synchronized TypeMarshallingDescriptor getDescriptor(Object instance) {
		Class<?> clazz = instance.getClass();
		TypeMarshallingDescriptor desc = cache.get(clazz);
		if (desc == null) {
			desc = new TypeMarshallingDescriptor(clazz);
			initDescriptor(clazz, desc);
			cache.put(clazz, desc);
		}
		return desc;
	}

	/** init handlers for all annotated object properties */
	protected void initDescriptor(Class<?> beanClass, TypeMarshallingDescriptor desc) {
		try {
			Method[] methods = beanClass.getDeclaredMethods();
			Set<String> prepared = new HashSet<String>();
			for (Method method : methods) {
				if (method.isAnnotationPresent(JSONField.class)) {
					PropertyDescriptor<?> pDesc = PropertyDescriptor.valueOf(beanClass, method);
					String pName = pDesc.getName();
					// Prevent handler duplication when annotating both setter
					// and getter
					if (!prepared.contains(pName)) {
						desc.addFieldDecl(createField(pDesc));
						prepared.add(pName);
					}
				}
			}
		} catch (Exception ex) {
			throw new IllegalStateException(String.format("marshaller descriptor initialization failed on type %s",
					beanClass.getName()), ex);
		}
	}

	/** create JSON field handler instance of specified property descriptor */
	protected JSONFieldDecl<?> createField(PropertyDescriptor<?> desc) throws InstantiationException,
			IllegalAccessException {
		if (isDefaultFactory(desc)) {
			// FIXME [LKo] fix usage of generic parameters
			for (JSONFieldDeclFactory factory : factories) {
				if (factory.isApplicable(desc)) {
					return factory.create(desc, this);
				}
			}
			throw new IllegalStateException("JSONFieldHandler for " + desc.getName() + " not found");
		} else {
			JSONFieldDeclFactory factory = desc.getAnnotation(JSONField.class).factory().newInstance();
			return factory.create(desc, this);
		}
	}

	/** check if annotation points to default factory */
	protected boolean isDefaultFactory(PropertyDescriptor<?> desc) {
		return JSONFieldDeclFactory.class.getClass().equals(desc.getAnnotation(JSONField.class).factory().getClass());
	}

}
